package dbMysql

import (
	"fmt"
	"gitee.com/kingzyt/common/log"
	. "gitee.com/kingzyt/common/util"
	"regexp"
	"sort"
	"strconv"
	"sync"
	"time"
)

/*
这里为什么不用mysql分区来处理的原因：
分区最方便的用法是按照主键直接hash，而要按照时间来按月划分的话，需要提前将可预见的未来的月份都划分好，这个无疑是一个不可确定的量
这里处理的问题往往需要将最近N个年/月/日的数据集中到少数表中处理，而整体的数据是一个不确定的不断增长的集合，我们不希望在处理数据的时候还涉及查询范围以外的可能的巨量旧数据
mysql没有提供自动为每一个年/月/日划分一张新表，同时只查询最新的N张新表的功能，所以这里提供的是一个不同于分区方式的分区插件
*/
/*
这里不能由外部干预，目前是直接由表的个数来对应推导日期的，如果中间有表格缺失，将会影响使用
*/
type NRecent struct {
	name            string
	tableNamePrefix string
	readTableNum    int
	keepTableNum    int

	querier *ShardingByDate

	tableDates      []int32
	tableDatesMutex sync.RWMutex
}

//tableNamePrefix_$date$
func NewNRecent(name string, tableNamePrefix string, readDateUnitNum int, keepDateUnitNum int, sqlRWer *SimpleMySqlRWer, mode ShardingByDateMode, createTableSqlTemplate string) *NRecent {
	rlt := &NRecent{
		name:            name,
		tableNamePrefix: tableNamePrefix,
		/* we read data from n+2 tables,
		use month mode for example, n is the latest n month,
		and 1 when we just created the new table right now, then we have no data in the latest table, but we also need to query datas for the latest month, so we at least need 2 tables,
		and the left 1 is for safty, because ShardingByDate may make mistakes when the data is inserted at the timepoint of table switching,
		the new data may be inserted into old table, so we need add anther 1 table to get exactly all datas we need
		*/
		readTableNum: readDateUnitNum + 2,
		keepTableNum: keepDateUnitNum,
		querier:      NewShardingByDate(name, sqlRWer, mode),
	}
	rlt.querier.AddQueryDef(0, rlt.tableNamePrefix+"_$date$", createTableSqlTemplate)
	if rlt.keepTableNum < rlt.readTableNum {
		rlt.keepTableNum = rlt.readTableNum
	}
	return rlt
}

func (self *NRecent) getTableHead() string {
	return self.tableNamePrefix + "_" + self.querier.GetTableDateMark()
}

func (self *NRecent) Start() bool {
	queryDescs, err := GetMysqlTableDescriptions(self.name, self.getTableHead()+"%", self.querier.GetSqlRW())
	if err != nil {
		log.Error(self.name, "get tables descriptions fail, err: %s", err)
		return false
	}

	tableNameRegexp := regexp.MustCompile("^" + self.getTableHead() + "(\\d+)$")
	for i := 0; i < len(queryDescs); i++ {
		qd := queryDescs[i]
		matches := tableNameRegexp.FindStringSubmatch(qd.TableName)
		if len(matches) != 1+1 {
			log.Error(self.name, "table %s name is invalid, should be like %s[\\d]+", qd.TableName, self.getTableHead())
			return false
		}

		date, _ := strconv.Atoi(matches[1])
		self.tableDates = append(self.tableDates, int32(date))
	}

	if len(queryDescs) > 0 {
		if !CheckTablesDescriptionSame(self.name, queryDescs) {
			return false
		}

		sort.SliceStable(self.tableDates, func(i, j int) bool {
			return self.tableDates[i] < self.tableDates[j]
		})

		log.Info(self.name, log.I, "there are tables for %v", self.tableDates)
	}

	self.querier.OnTableCreateOrCheckExisted = func(curDateMarkInSql string, curDateMarkInt int) {
		self.tableDatesMutex.Lock()
		defer self.tableDatesMutex.Unlock()
		if !AIdsContainsBId2(self.tableDates, int32(curDateMarkInt)) {
			log.Info(self.name, log.I, "add new table for [%d]", curDateMarkInt)
			self.tableDates = append(self.tableDates, int32(curDateMarkInt))
			sort.SliceStable(self.tableDates, func(i, j int) bool {
				return self.tableDates[i] < self.tableDates[j]
			})
		}
	}

	if !self.querier.Start() {
		return false
	}

	return true
}

func (self *NRecent) CheckDropOverdueTables() {
	self.tableDatesMutex.Lock()
	defer self.tableDatesMutex.Unlock()

	if len(self.tableDates) <= self.keepTableNum {
		return
	}

	sqlrw := self.querier.GetSqlRW()
	var drops []int32
	drops = append(drops, self.tableDates[:len(self.tableDates)-self.keepTableNum]...) // copy from tableDates because it will delete elements later
	for i := 0; i < len(drops); i++ {
		dropTarget := drops[i]
		found := false
		tableName := fmt.Sprintf("%s%d", self.getTableHead(), dropTarget)
		var t string
		err := sqlrw.ReadQueryRow("show tables like '"+tableName+"'", []interface{}{&t}, func(rowScanFace []interface{}) {
			found = true
		})
		if err != nil {
			log.Error(self.name, "drop overdue table fail, err:%s", err)
			continue
		}

		if found {
			_, err := sqlrw.WriteQuery("drop table " + tableName)
			if err != nil {
				log.Error(self.name, "drop overdue table fail2, err:%s", err)
				continue
			} else {
				log.Info(self.name, log.I, "drop overdue table %s ok", tableName)
			}
		} else {
			log.Warn(self.name, "drop target overdue table %s is not found", tableName)
		}

		self.tableDates, _ = IntListMultiDel(self.tableDates, dropTarget)
	}
}

func (self *NRecent) GetRecentTablesWithLock(proc func(sqlRWer *SimpleMySqlRWer, tableName string) bool) {
	self.tableDatesMutex.RLock()
	defer self.tableDatesMutex.RUnlock()

	var ts []int32
	if len(self.tableDates) > self.readTableNum {
		ts = self.tableDates[len(self.tableDates)-self.readTableNum:]
	} else {
		ts = self.tableDates
	}

	//fmt.Printf("%v, %v\n", ts, self.tableDates)
	for i := len(ts) - 1; i >= 0; i-- {
		tableName := self.getTableHead() + strconv.Itoa(int(ts[i]))
		//fmt.Println(i, tableName)
		if !proc(self.querier.GetSqlRW(), tableName) {
			break
		}
	}
}

func (self *NRecent) GetLatestTableName(now time.Time) (sqlRWer *SimpleMySqlRWer, tableName string, ok bool) {
	tableName, ok = self.querier.GetQuerySqlStr(0, now.UnixNano())
	return self.querier.GetSqlRW(), tableName, ok
}
